


import uiautomator2 as u2
from uiautomator2.watcher import Watcher
from uiautomator2.exceptions import UiObjectNotFoundError
from typing import Tuple, Dict, List
from time import sleep
import time
from random import random, randint, uniform
from copy import deepcopy
from numpy import percentile, sqrt
from base import RetryException


class MyDecorator:
    
    def __init__(self):
        self.log = None
        self.u = None
        self.note_error = {} # 记录错误次数
    def error(self, exception_desc: str):
        """异常装饰器
        Args：
            messages：传入传递消息类，
            which_exception：异常的种类,
            exception:异常的描述"""
        def func_outer(func):
            def func_inner(*args, **kwargs):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    # print(e, exception_desc)
                    error_info = f'Auto错误-{exception_desc}:{e}'
                    # self.log.info(error_info )
                    # 错误次数在一分钟内超过7次则报错重启
                    now_time = int(time.strftime("%H%M"))
                    # 一分钟内报错7次，报严重错误
                    if not self.note_error.get(now_time):
                        self.note_error[now_time] = 1
                    else:
                        self.note_error[now_time] += 1
                    if self.note_error[now_time] > 30:
                        self.log.info('一分钟报错超30次，重启')
                        del self.note_error[now_time]
                        raise RetryException
                    # 单错误累计7次，报严重错误
                    if not self.note_error.get(error_info):
                        self.note_error[error_info] = 1
                    else:
                        self.note_error[error_info] += 1
                    if self.note_error[error_info] > 10:
                        del self.note_error[error_info]
                        self.log.info(error_info+'错误超过十次')
                        raise RetryException()
            return func_inner
        return func_outer
    
    def watcher_(self, name:str, obj:str):
        """观察者装饰器, obj可以是text和xpath,"""
        def func_outer(func):
            def func_inner(*args, **kwargs):
                self.u.creat_watcher(name, obj)
                self.log.info(f'创建观察者{name}')
                func(*args, **kwargs)
                self.u.remove_watcher(name)
                self.log.info(f'创建观察者{name}')
            return func_inner 
        return func_outer
    
    def watchers_(self, names:List, objs:List):
        """观察者装饰器, objs可以是text和xpath,如果传入多个，请分别用列表分别包装"""
        def func_outer(func):
            def func_inner(*args, **kwargs):
                for name, obj in zip(names, objs):
                    self.u.creat_watcher(name, obj)
                    self.log.info(f'创建观察者{name}')
                func(*args, **kwargs)
                for name, obj in zip(names, objs):
                    self.u.remove_watcher(name)
                    self.log.info(f'创建观察者{name}')
            return func_inner 
        return func_outer
    
    def affirm(self, what: str, pre_sleep_time: int, cycle_sleep_time: int, times: int=3):
        """装饰器，循环确认是否登录成功"""
        def func_outer(func):
            def func_inner(*args, **kwargs):
                sleep(pre_sleep_time)
                for i in range(times):
                    self.log.info(f'第{i}次尝试{what}')
                    if not func(*args, **kwargs):
                        sleep(cycle_sleep_time)
                    else:
                        self.log.info(f'完成{what}')
                        break
                else:
                    self.log.info(f'未完成{what},请检查，将重启应用')
                    raise RetryException
            return func_inner 
        return func_outer
       
wraper = MyDecorator()
error_decorator = wraper.error
affirm_decorator = wraper.affirm
watcher_decorator = wraper.watcher_
watchers_decorator = wraper.watchers_

"""watcher很有用，但是无法知道哪个被触发，所以我再Watcher类里面增加了，trriggered
的字典属性，来记录哪个被触发。具体代码增加在 _run_watchers ,self.triggered.update({h['name']:True})
remove, del self.triggered[name]"""
class MyAuto:
    """自动化基础工具类"""
    def __init__(self, mobile_device: str, mobile_url=None):
        # 创建实例
        if mobile_url:
            d = u2.connect_wifi(mobile_url)
        else:
            d = u2.connect(mobile_device)
        # 打开屏幕
        d.screen_on()
        # 解锁屏幕
        d.unlock()
        # d.app_stop_all()
        # 设置默认元素等待超时(秒)
        # d.wait_timeout = wait_second
        # 设置每次UI点击后1.5秒的延迟
        #修改延迟为操作前延迟2S 操作后延迟4.5S
        d.settings['operation_delay'] = (1.5, 1.5)
        # 是否打印日志
        # d.settings['xpath_debug'] = False
        #修改延迟生效方法
        # d.settings['operation_delay_methods'] = {'click','press','send_keys'}
        # 修改默认等待
        # d.settings['wait_timeout'] = 10
        # d.implicitly_wait(wait_second)
        self.d = d
        self.package_name =''
        self.xy = self.d.window_size()  # 屏幕尺寸
        self.mobile_device = mobile_device
    # 图片识别不佳，经常错误点击
    # @error_decorator('点击图片')
    # def click_p(self, png_path, timeout=5):
    #     if self.u.d.image.click(png_path, timeout):
    #         return True
    #     else:
    #         return False
        
    # @error_decorator('双击图片')
    # def double_click_p(self, png_path, timeout=3):
    #     pass
    
    # @error_decorator('等待图片')
    # def wait_p(self, png_path, record_pos:Tuple, resolution:Tuple, timeout=40):
    #     if self.u.d.image.wait(png_path, timeout):
    #         return True
    #     else:
    #         return False
    
               
    def quit_driver(self):
        self.d.service("uiautomator").stop()
        return True

    def back(self):
        self.d.press("back")
        return True
    
    def open_app(self, package_name:str):
        self.d.app_start(package_name, stop=True)
        self.package_name = package_name
        return True
        
    def close_app(self, package_name:str):
        self.d.app_stop(package_name)
        return True
        
    def close_other_app(self, retain_package_name:str):
        self.d.app_stop_all(excludes=[retain_package_name])
        return True

    @error_decorator('翻页')
    def swipe_page(self, direction:str = 'down', pct: float=0.7):
        """随机选择起点，按固定比例移动页面"""
        x, y = self.xy
        base_center = round(uniform(0.4, 0.6), 2)
        base_from_v = round(uniform(0.1, 0.15), 2)
        base_from_h = round(uniform(0.2, 0.25), 2)
        r = round(uniform(0.05, 0.1), 2)
        down = (base_center*x, (pct+r)*y, base_center*x, base_from_h*y)
        up = (base_center*x, base_from_h*y, base_center*x, (pct+r)*y)
        left = (base_from_v*x, base_center*y, (pct+r)*x, base_center*y)
        right = ((pct+r)*x, base_center*y, base_from_v*x, base_center*y)
        if direction == 'up':
            self.d.swipe(*up)
        elif direction == 'down':
            self.d.swipe(*down)
        elif direction == 'right':
            self.d.swipe(*right)
        elif direction == 'left':
            self.d.swipe(*left)
        else:
            raise Exception('输入错误')
        return True
        
    @error_decorator('滑动')
    def scroll_(self,direction:str = 'down', steps: int=20):
        """这方法不涉及坐标，无法随机选择，最好少用"""
        if direction == 'up':
            self.d(scrollable=True).scroll(steps=steps)
        elif direction == 'down':
            self.d(scrollable=True).scroll.vert.backward(steps=steps)
        elif direction == 'left':
             self.d(scrollable=True).scroll.horiz.forward(steps=steps)  
        elif direction == 'right':
             self.d(scrollable=True).scroll.horiz.backward(steps=steps) 
        else:
            raise Exception('输入错误')
        return True
    
 
    @error_decorator('单击相对坐标')
    def tap(self, pxy:Tuple):
        x, y = pxy
        self.d.click(x=x, y=y)
        return True
        
    @error_decorator('双击相对坐标')
    def double_tap(self,pxy:Tuple, xy_=(1080, 1920), calc=False):
        # u2的double_click不起作用。
        if calc:
            pxy = self._calc_xy(pxy, xy_)
        x, y = pxy
        self.d.double_click(x=x, y=y)
        return True
    
    @error_decorator( '点击')
    def click_(self, name: str, by_which: str='text', timeout=2,
                 contains=False):
        if result := self.find(name, by_which, timeout, contains):
            result.click()
            return True
        else:
            return False

    @error_decorator('等待存在点击')
    def long_wait_click(self, name, by_which='text', timeout=60, contains=False):
        # 过早执行可能会直接返回错误，而不等待
        sleep(5)
        if result := self.find(name, by_which, timeout, contains):
            result.click()
            return True
        else:
            return False
       
    @error_decorator('等待消失')
    def wait_noexist(self, name, by_which='text', timeout=60, contains=False):
        sleep(2)
        if result := self.find(name, by_which, 2, contains):
            result.wait_gone(timeout)
            return True
        else:
            return False
        
    @error_decorator('确认存在')
    def find(self, name, by_which='text', timeout=2, contains=False):
        result = None
        if not contains:
            if by_which == 'text':
                if not self.d(text=name).exists(timeout):
                    if self.d(description=name).exists(0.5):
                        result = self.d(description=name)
                else:
                    result = self.d(text=name)
            elif by_which == 'id':
                if self.d(resourceId=f'{self.package_name}:id/{name}').exists(timeout):
                    result = self.d(resourceId=f'{self.package_name}:id/{name}')
            elif by_which == 'class':
                if self.d(className=name).exists(timeout):
                    result = self.d(className=name)
            elif by_which == 'xpath':
                sleep(timeout)
                # xpath 不能用exists()确定是否存在，要用exists
                if self.d.xpath(name).exists:
                    result = self.d.xpath(name)
            else:
                raise Exception ('by_which输入有误')
        elif contains:
            if by_which == 'text':
                if not self.d(textContains=name).exists(timeout):
                    if self.d(descriptionContains=name).exists(0.5):
                        result = self.d(descriptionContains=name)
                else:
                    result = self.d(textContains=name)
            else:
                raise Exception ('by_which输入有误')
        return result
        
    @error_decorator('获取单个内容')
    def attribute(self, name: str, by_which: str='text', attribute:str='text', timeout=3,
                    contains=False) -> str:
        attr = None
        if attribute == 'text':
            if result := self.find(name, by_which, timeout, contains):
                if not (attr := result.info[attribute]):
                    attr = result.info['contentDescription']
        else:
            attr = self.find(name, by_which, timeout, contains).info[attribute]
        return attr
    
    @error_decorator('获取多个内容')
    def attributes(self, name: str, by_which: str='text',  attribute:str='text', timeout=3,
                    contains=False) -> List:
        texts = []
        if  result := self.find(name, by_which, timeout, contains):
            for elem in result:
                if attribute == 'text':
                    if not (attr := elem.info['text']):
                        attr = elem.info['contentDescription']
                else:
                    attr = elem.info[attribute]
                if attr:
                    texts.append(attr)
        return texts
        
    @error_decorator('class获取多个内容')
    def class_attributes(self, class_name:str, attribute:str='text') -> List:
        texts = []
        for elem in self.d.xpath(f'//{class_name}').all():
            if attribute == 'text':
                if not (attr := elem.info['text']):
                    attr = elem.info['contentDescription']
                else:
                    attr = elem.info[attribute]
            if attr:
                texts.append(attr)
        return texts
   
    @error_decorator('获取提示信息')
    def get_message(self, timeout=3):
        return self.d.toast.get_message(wait_timeout=timeout)
    
    @error_decorator('输入文字')
    def send_text(self, content: str, chinese=False):
        if chinese:
            self.d.set_fastinput_ime(True)
            self.d.send_keys(content)
            self.d.set_fastinput_ime(False)
        else:
            self.d.send_keys(content)
            
    @error_decorator('创建观察者')
    def creat_watcher(self, name, obj):
        self.d.watcher(name).when(obj).click()
    
    @error_decorator('移除观察者')
    def remove_watcher(self, name):
        self.d.watcher.remove(name)

    def affirm_watched(self, name):
        pass
    """
    因为watcher没有返回哪个被触发的方法，所以我在watcher里面增加了triggered属性，
    来记录哪个被触发。self.triggered = {}  def _run_watchers下self.triggered.update({h['name']:True})
    和 def remove 下   if self.triggered.get(name):
    """
        
    """
    1、截图
    uiautomator2 screenshot screenshot.jpg  截图保存至当前文件夹，命名为screenshot.jpg
    uiautomator2 screenshot E:\liang\tools\screenshot.jpg  截图保存至指定文件夹，命名为screenshot.jpg
2、获取当前包名和activity
    uiautomator2 current
    {
        "package": "com.android.browser",
        "activity": "com.uc.browser.InnerUCMobile",
        "pid": 28478
    }
3、卸载
    uiautomator2 uninstall <package-name> # 卸载一个包
    uiautomator2 uninstall <package-name-1> <package-name-2> # 卸载多个包
    uiautomator2 uninstall --all # 全部卸载
4、stop: 停止应用
    uiautomator2 stop com.example.app # 停止一个app
    uiautomator2 stop --all # 停止所有的app
    1、启动/关闭APP
    5、 运行/关闭app
d.app_start('包名')
d.app_stop('包名')
d.app_clear('包名')
扩展：不知道怎么获取包名的可以打开一个app，doc下输入uiautomator2 current获取
    {
        "package": "com.android.browser",
        "activity": "com.uc.browser.InnerUCMobile",
        "pid": 28478
    }
        手势交互
    点击屏幕（坐标，支持相对坐标）
        d.click(x, y)
        d.click(0.5, 0.5)  #其中(0.235, 0.456) 代表 X(50%) Y(50%)
    双击
        d.double_click(x, y)  #默认两次点击相隔0.1秒
        d.double_click(x, y, 1) #两次点击相隔1秒
    长按
        d.long_click(x, y)  #默认按0.5秒
        d.long_click(x, y, 5) # l长按5秒
    滑动（滑动屏幕）
        d.swipe(sx, sy, ex, ey)  从坐标（sx，sy）滑动至坐标（ex,ey），支持相对坐标
        d.swipe(sx, sy, ex, ey, 0.5) # 默认滑动时间0.5秒，可修改
    SwipeExt 扩展功能
        d.swipe_ext("right") # 屏幕右滑，4选1 "left", "right", "up", "down"
        d.swipe_ext("right", scale=0.9) # 默认0.9, 滑动距离为屏幕宽度的90%
        d.swipe_ext("right", box=(0, 0, 100, 100)) # 在 (0,0) -> (100, 100) 这个区域做滑动
    拖动（例:拖动某个APP）
        d.drag(sx, sy, ex, ey)  #支持相对坐标
        d.drag(sx, sy, ex, ey, 0.5) # 默认滑动时间0.5秒，可修改
    多个点连续滑动（典型案例：9宫格解锁）
        从point(x0, y0) 滑动到point(x1, y1)然后滑动到point(x2, y2)...等等
        d.swipe_points([(x0, y0), (x1, y1), (x2, y2)], 0.2))  #0.2为每次两点之间滑动的时间，坐标可以是绝对坐标，也可以是相对坐标
        d.swipe_points([(0.235, 0.456), (0.503, 0.449), (0.509, 0.601), (0.777, 0.603), (0.771, 0.763), (0.222, 0.75)], 0.2)  #其中(0.235, 0.456) 代表 X(23.5%) Y(45.6%)
    模拟按下-等待-移动-松开
        d.touch.down(10, 10) # 模拟按下
        time.sleep(0.01) # down 和 move 之间的延迟，自己控制
        d.touch.move(15, 15) # 模拟移动
        d.touch.up() # 模拟抬起
    截图
        d.screenshot("E:\liang\tools\screenshot.jpg")传入路径和名称

元素选择器
    #定位text为'Clock'并且className为'android.widget.TextView'的元素
    d(text='Clock', className='android.widget.TextView')   
    后代元素
        #定位className为"android.widget.ListView"的元素下面text为"Bluetooth"的元素
        d(className="android.widget.ListView").child(text="Bluetooth")
    兄弟姐妹，同级元素
        #定位text为"Google"的元素的同级元素中className="android.widget.ImageView"的元素
        d(text="Google").sibling(className="android.widget.ImageView")
    通过多个层级多个属性定位
        #className="android.widget.ListView"且resourceId="android:id/list"的元素下面text为"Bluetooth"且className="android.widget.LinearLayout"的元素
        d(className="android.widget.ListView", resourceId="android:id/list").child_by_text("Bluetooth", className="android.widget.LinearLayout")
        可以由多个层级往下定位，例：
            d(className="###", resourceId="###").child_by_text("Wi-Fi", className="￥￥￥￥").child(className="****").click()
    相对定位
        d(A).left(B), 定位A左边的B
        d(A).right(B), 定位A右边的B
        d(A).up(B), 定位A上边的B
        d(A).down(B), 定位A下边的B
    多个同属性元素索引
        如果元素选择器选择到了多个同属性的元素而无法进行更细致的区分，可以用索引选择指定的元素
        d(text="Add new", instance=0)   #instance=0表示选择第一个，依次类推
        其他操作：
            d(text="Add new").count   #返回当前屏幕某个属性元素的个数
            len(d(text="Add new"))    #返回当前屏幕某个属性元素列表的长度
            d(text="Add new")[0]      #用下标选择指定元素，同d(text="Add new", instance=0)
            迭代：
                for view in d(text="Add new"):
                    view.info
    获取所选ui对象的状态及其信息
        1、检查元素是否存在
            d(text="Settings").exists # 返回布尔值，True if exists, else False
            d(text="Settings").exists(timeout=3)   #增加等待时间为3秒
        2、输出指定元素的信息
            d(text="Settings").info
                
        3、获取、输入、清除输入框文本
            d(text="Settings").get_text()  # 获取文本内容
            d(text="Settings").set_text("My text...")  #输入文本内容
            d(text="Settings").clear_text()  # 清除文本内容
        4、获取指定元素中心点坐标
            x, y = d(text="Settings").center()
            x, y = d(text="Settings").center(offset=(0, 0)) # offset为指定元素的相对坐标，(0,0)表示元素左上角，（0.5,0.5）表示元素中心，（1,1）表示元素右下角
指定元素点击操作
    d(text="Settings").click()  #点击指定元素中心位置
    d(text="Settings").click(timeout=10)  #等待元素出现（最多等待10秒）后点击
    d(text="Settings").click(offset=(0.5, 0.5)) # offset为指定元素的相对坐标，(0,0)表示元素左上角，（0.5,0.5）表示元素中心，（1,1）表示元素右下角
    clicked = d(text='Skip').click_exists(timeout=10.0)  #如果10秒内元素存在，则点击，默认等待10秒
    is_gone = d(text="Skip").click_gone(maxretry=10, interval=1.0)  #等待元素消失后点击，返回布尔值，默认轮询次数10次，每次间隔时间1秒
    d(text="Settings").long_click()  #长按指定元素
特定元素的手势操作
    1、将元素拖向另一个点或另一个元素
        备注：安卓4.3以下不能用
        d(text="Settings").drag_to(x, y, duration=0.5)  #将指定元素在0.5秒的时间内拖动至指定坐标
        d(text="Settings").drag_to(text="Clock", duration=0.25) #将指定元素在0.25秒的时间内拖动至指定元素的中心位置
    2、等待元素出现或消失
        d(text="Settings").wait(timeout=3.0) #等待元素出现，等待时间最长3秒，返回布尔值，默认等待时间20秒
        d(text="Settings").wait_gone(timeout=1.0)  #等待元素消失，等待时间最长1秒，返回布尔值，默认等待时间20秒
    3、滚动屏幕          
        scroll()里面的参数steps默认是滑动一个屏幕的距离
        a.向上滚动：d(scrollable=True).scroll(steps=10)
        b.向下滑动：d(scrollable=True).scroll.vert.backward()         
        c.水平向右滚动：d(scrollable=True).scroll.horiz.forward(steps=50)       
        d.水平向左滚动：d(scrollable=True).scroll.horiz.backward(steps=50)      
        e.水平滑动到最左边：d(scrollable=True).scroll.horiz.toBeginning(steps=100,         max_swipes=1000)       
        f.水平滑动到最右边：d(scrollable=True).scroll.horiz.toEnd(steps=100, max_swipes=1000)         
        g.竖直滑动到结尾：d(scrollable=True).scroll.toEnd()      
        h.竖直滑动到开头：d(scrollable=True).scroll.toBeginning(steps=50)        
        i.滑动到指定位置：d(scrollable=True).scroll.to(text="Security")
全局设置
    # 设置每次UI点击后1.5秒的延迟
    d.click_post_delay = 1.5 # default no delay
    
    # 设置默认元素等待超时(秒)
    d.wait_timeout = 30.0 # default 20.0
    
    设置元素查找等待时间（默认20s）
    d.implicitly_wait(10.0)
输入内容
    d.send_keys('str')
Toast
    1、显示toast
    d.toast.show("Hello world", 1.0) # show for 1.0s, default 1.0s
    2、获取toast
    # 5.0: 最大等待时间
    # 10.0: toast出现后的缓存时间. 默认 10.0
    # "default message": 返回的toast值. Default None
    d.toast.get_message(5.0, 10.0, "default message")
    # 一般用法
    assert "Short message" in d.toast.get_message(5.0, default="")
    # 清除toast缓存
    d.toast.reset()
xpath
    description -> content-desc（xpath）
    resourceId -> resource-id（xpath）
    # 等待元素存在（等待10秒）
    d.xpath("//android.widget.TextView").wait(10.0) # return bool
    # 找到元素并点击
    d.xpath("//*[@content-desc='分享']").click()
    # 检查是否存在
    if d.xpath("//android.widget.TextView[contains(@text, 'Se')]").exists:
        print("exists")
    # 获取所有输入框的text，属性和中心点坐标
    for elem in d.xpath("//android.widget.TextView").all():
        print("Text:", elem.text)
        # Dictionary eg
        #{'index': '1', 'text': '999+', 'resource-id':   'com.netease.cloudmusic:id/qb', 'package': 'com.netease.cloudmusic', 'content-desc': '', 'checkable': 'false', 'checked': 'false', 'clickable': 'false','enabled': 'true', 'focusable': 'false', 'focused': 'false','scrollable': 'false','long-clickable': 'false', 'password': 'false', 'selected': 'false', 'visible-to-user': 'true', 'bounds': '[661,1444][718,1478]'}'''
        print("Attrib:", elem.attrib)
        # Coordinate eg: (100, 200)
        print("Position:", elem.center())
    """